# 页面埋点实践

埋点即为监控用户行为而预先埋设的统计代码。埋点的用途很多,视场景而定,例如统计访问入口、访问量、点击量、停留时间、异常监控等等,它是用户调研的依据,是产品效果的佐证,对产品迭代至关重要。

# 一、埋点方式

常见的埋点方式有以下几种:

1. 代码埋点

所谓代码埋点,就是在需要统计数据的地方,植入发送埋点请求的代码,直接统计用户交互行为。 优点:针对性强,可实现精确埋点,且灵活度高,可针对几乎任何交互行为设置各种埋点信息。 缺点:业务代码与埋点代码耦合性强,工作量大,更新迭代代价大。

2. 无痕埋点

无痕埋点并非不写代码,而是以高度解耦的形式,利用事件代理 + 元素路径的方式,实现非侵入无感知的埋点。 优点:高度解耦,工作量小,易维护。 缺点:记录行为信息少,不能禁用元素的事件冒泡。

3. 后端埋点

后端埋点顾名思义,就是由服务器端、实现的埋点,主要针对数据交互行为的埋点,无法对用户非请求性行为埋点。 优点:可通过服务端日志获取埋点信息,减少请求数。 缺点:可监控行为类别较少,无法全方位监控用户行为。

# 二、埋点实现

本文仅介绍前端页面中的埋点实现,即代码埋点无痕埋点。 目前,针对埋点信息的通常做法是利用img标签的src属性,携带埋点数据传输到后台,进而针对数据做统计分析。

1. 代码埋点

原理:事件触发时,创建一个img标签,在其src中携带想要传输的埋点数据,例如用户名、元素的唯一标识id等。 以点击事件触发埋点用以统计点击量为例,伪代码实现如下:

// 埋点核心
class Stat {
  createImg () {
    const IMG = document.createElement('img)
    IMG.src = `image.baidu.com/test/demo.png?user_name=${obj.user_name}&ele_id=${obj.ele_id}`
    docuemnt.body.appendChild(IMG)
  }

  log () {

  }
}

// 点击时调用,触发埋点
const ST = new Stat()
document.getElementById('#id-name').addEventListener('click', () => {
  ...
  ST.log({
    user_name: userName,
    ele_id: '123456'
  })
})

2. 无痕埋点

原理(以点击为例):点用户点击时,获取元素到body之间整个dom的路径,作为该元素的唯一标识,称之为domPath,并将其与埋点数据一同传输至服务端。伪代码实现如下: (参考:https://github.com/airuikun/smart-tracker)

// body绑定事件,捕获全局点击
document.body.addEventListener('click', (event) => {
  this.handleEvent(event);
}, false)

// 事件回调
handleEvent(event) {
  const domPath = getDomPath(event.target);
  const data = {
    domPath: domPath,
    trackingType: event.type
  };
  this.send(data);
}

// 获取元素唯一标识即dom路径domPath
getDomPath(element) {
  let domPath = [];
  let elem = element;
  while (elem) {
    let domDesc = getDomDesc(elem);
    domPath.unshift(domDesc);
    elem = elem.parentNode;
  }
  return domPath.join('>');   // eg. html>body>div[name=testwrapper]>#test>#btn 
}

// 获取每一级元素的描述
getDomDesc(element, useClass = false) {
  const domDesc = [];
  if (!element || !element.tagName) {
    return '';
  }
  if (element.id) {
    return `#${element.id}`;
  }
  domDesc.push(element.tagName.toLowerCase());
  if (useClass) {
    const className = element.className;
    if (className && typeof className === 'string') {
      const classes = className.split(/\s+/);
      domDesc.push(`.${classes.join('.')}`);
    }
  }
  if (element.name) {
    domDesc.push(`[name=${element.name}]`);
  }
  return domDesc.join('');
}

// 发送埋点信息
send(data = {}) {
  let image = new Image(1, 1);
  image.onload = function () {
    image = null;
  };
  image.src = `/?${stringify(data)}`;
}
最后更新时间: 4/27/2021, 7:11:19 PM